/*
 * nLayers (c) Created on Jan 29, 2004
 */
package dnl.util.ui.table.sort;


import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;

import dnl.util.ui.table.RowComparator;
import dnl.util.ui.table.RowData;
import dnl.util.ui.table.TModel;

import ajul.comparators.IntegerComparator;
import ajul.comparators.LongComparator;
import ajul.comparators.StringComparator;



/**
 * Adds the functionality of sorting to any <code>JTable</code>.
 * If the table's header is visible, sorting will take place whenever 
 * pressing the column headers.<br>
 * The <code>TableSorter</code> functionality
 * may also be invoked programmatically.
 * 
 * Note that sorting happens by copying the model's data, sorting it and 
 * then reinserting it to the model. This may be (have'nt tried though) 
 * a bit slow.    
 * 
 */
public final class TableSorter  {
	
	private static final SortDirection DEFAULT_SORT_ORDER = SortDirection.Descending;

	private final JTable table;
	private final Icon upIcon;
	private final Icon downIcon;
	
	// The sort as currently displayed to the user, representing the end result
    // of a previous user request.
	private SortBy currentSort;
	private SortBy targetSort;
	
	private Map<Integer, Comparator<?>> predefinedTypedComparators = new HashMap<Integer, Comparator<?>>();

	private RowComparator rowComparator = new RowComparator(0, false, new StringComparator());	
	
	/**
	 * @param aTable
	 *            receives indication of table sort ; if it has any custom
	 *            header renderers, they will be overwritten by this class.
	 * @param aUpIcon
	 *            placed in column header to indicate ascending sort.
	 * @param aDownIcon
	 *            placed in column header to indicate descending sort.
	 */
	public TableSorter(JTable aTable) {
		table = aTable;
		upIcon = getIcon("order_asc.gif");
		downIcon = getIcon("order_des.gif");
		currentSort = SortBy.NONE;
		initHeaderClickListener();
		initHeaderRenderers();
		if (getRenderer(0) == null) {
			throw new AssertionError("Ctor - renderer 0 is null.");
		}
	}

	public void setPredefinedComparator(int column, Comparator<?> comparator){
		predefinedTypedComparators.put(new Integer(column), comparator);
	}
	
	private Icon getIcon(String fname){
		try {
			URL url = TableSorter.class.getResource("images/"+fname);
			ImageIcon ii = new ImageIcon(url);
			return ii;
		} catch (Exception e) { // any failure loading the image
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * @param column
	 */
	private void orderRowsByColumn() {
		
		TableModel tableModel = table.getModel();
		int column = targetSort.getColumn();
		Class<?> columnClass = tableModel.getColumnClass(column);

		Comparator<?> comparator = predefinedTypedComparators.get(new Integer(column));
		if(comparator != null){
			this.rowComparator.setTypedComparator(comparator);
		}else if(Number.class.isAssignableFrom(columnClass)){
			if(columnClass.equals(Integer.class)){
				this.rowComparator.setTypedComparator(new IntegerComparator());
			}
			if(columnClass.equals(Long.class)){
				this.rowComparator.setTypedComparator(new LongComparator());
			}
		}
		else {
			this.rowComparator.setTypedComparator(new StringComparator());
		}
		RowData[] data = cloneModelData();
		Arrays.sort(data, rowComparator);
		applyOrderedData(data);		
	}	
		
	/**
	 * 
	 * @param newData
	 */
	private void applyOrderedData(RowData[] newData){
		TableModel tableModel = table.getModel();
		if(tableModel instanceof TModel){
			((TModel)tableModel).setData(newData);
			return;
		}
	}
	
	/**
	 * 
	 * @return
	 */
	private RowData[] cloneModelData(){
		TModel tModel = (TModel)table.getModel();
		return tModel.getData();
	}
	
	
	/**
	 * Return the identity of column having the primary sort, and the direction
	 * of its sort.
	 */
	public SortBy getSortBy() {
		return currentSort;
	}

	/**
	 * Change the sort programmatically, instead of through a user click.
	 * 
	 * <P>
	 * If there is a user preference for sort, it is passed to this method.
	 * Notifies listeners of the change to the sort.
	 */
	public void setSortBy(SortBy aTargetSort) {
		validateIdx(aTargetSort.getColumn());
		initHeaderRenderers();
		if (getRenderer(0) == null) {
			throw new AssertionError("setSortBy - renderer 0 is null.");
		}
		targetSort = aTargetSort;
		if (currentSort == SortBy.NONE) {
			setInitialHeader();
		} else if (currentSort.getColumn() == targetSort.getColumn()
				&& currentSort.getOrder() != targetSort.getOrder()) {
			toggleIcon();
		} else {
			updateTwoHeaders();
		}
		synchCurrentSortWithSelectedSort();
		notifyAndPaint();
	}
	
	public void applySort(){
	    this.notifyAndPaint();
	}

	/**
	 * Return true only if the index is in the range 0..N-1, where N is the
	 * number of columns in fTable.
	 */
	private boolean isValidColumnIdx(int aColumnIdx) {
		return 0 <= aColumnIdx && aColumnIdx <= table.getColumnCount() - 1;
	}

	private void validateIdx(int aSelectedIdx) {
		if (!isValidColumnIdx(aSelectedIdx)) {
			throw new IllegalArgumentException("Column index is out of range: " + aSelectedIdx);
		}
	}

	/**
	 * Called both upon construction and by {@link #setSortBy}.
	 * 
	 * If fireTableStructureChanged is called, then the original headers are
	 * lost, and this method must be called in order to restore them.
	 */
	private void initHeaderRenderers() {
		/*
		 * Attach a default renderer explicitly to all columns. This is a
		 * workarounds for the unusual fact that TableColumn.getHeaderRenderer
		 * returns null in the default case; Sun did this as an optimization
		 * for tables with very large numbers of columns. As well, there is
		 * only one default renderer instance which is reused by each column.
		 * See
		 * http://developer.java.sun.com/developer/bugParade/bugs/4276838.html
		 * for further information.
		 */
		for (int idx = 0; idx < table.getColumnCount(); ++idx) {
			TableColumn column = table.getColumnModel().getColumn(idx);
			column.setHeaderRenderer(new Renderer(table.getTableHeader()));
			if (column.getHeaderRenderer() == null) {
				throw new AssertionError("Header Renderer is null");
			}
		}
	}

	private Renderer getRenderer(int aColumnIdx) {
		TableColumn column = table.getColumnModel().getColumn(aColumnIdx);
		return (Renderer) column.getHeaderRenderer();
	}

	private void initHeaderClickListener() {
		table.getTableHeader().addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent event) {
				int selectedIdx = table.getColumnModel().getColumnIndexAtX(event.getX());
				processClick(selectedIdx);
			}
		});
	}

	/**
	 * Update the display of table headers to reflect a new sort, as indicated
	 * by a mouse click performed by the user on a column header.
	 * 
	 * If <code>aSelectedIdx</code> is the column which already has the sort
	 * indicator, then toggle the indicator to its opposite state (up -> down,
	 * down -> up). If <code>aSelectedIdx</code> does not already display a
	 * sort indicator, then add a down indicator to it, and remove the
	 * indicator from the fCurrentSort column, if present.
	 */
	private void processClick(int aSelectedIdx) {
		validateIdx(aSelectedIdx);
		if (currentSort.getColumn() == aSelectedIdx) {
			targetSort = new SortBy(currentSort.getOrder().toggle(), aSelectedIdx);
		} else {
			targetSort = new SortBy(DEFAULT_SORT_ORDER, aSelectedIdx);
		}
		if (currentSort == SortBy.NONE) {
			setInitialHeader();
		}
		if (currentSort.getColumn() == targetSort.getColumn()) {
			toggleIcon();
		} else {
			updateTwoHeaders();
		}
		synchCurrentSortWithSelectedSort();
		notifyAndPaint();
	}

	private void notifyAndPaint() {
		rowComparator.setAscending(SortDirection.Ascending.equals(targetSort.getOrder()));
		rowComparator.setColumn(targetSort.getColumn());
		orderRowsByColumn();				
		table.getTableHeader().resizeAndRepaint();
	}

	private void setInitialHeader() {
		if (targetSort.getOrder() == SortDirection.Descending) {
			getRenderer(targetSort.getColumn()).setIcon(downIcon);
		} else {
			getRenderer(targetSort.getColumn()).setIcon(upIcon);
		}
	}

	/**
	 * Flip the direction of the icon (up->down or down->up).
	 */
	private void toggleIcon() {
		Renderer renderer = getRenderer(currentSort.getColumn());
		if (currentSort.getOrder() == SortDirection.Ascending) {
			renderer.setIcon(downIcon);
		} else {
			renderer.setIcon(upIcon);
		}
	}

	/**
	 * Change the fCurrentSort column to having no icon, and change the
	 * fTargetSort column to having a down icon.
	 */
	private void updateTwoHeaders() {
		getRenderer(currentSort.getColumn()).setIcon(null);
		getRenderer(targetSort.getColumn()).setIcon(downIcon);
	}

	private void synchCurrentSortWithSelectedSort() {
		currentSort = targetSort;
	}

	/**
	 * Renders a column header with an icon.
	 * 
	 * This class duplicates the default header behavior, but there does not
	 * seem to be any other option, since such an object does not seem to be
	 * available from JTableHeader.
	 */
	private final class Renderer extends DefaultTableCellRenderer {
		Renderer(JTableHeader aTableHeader) {
			setHorizontalAlignment(JLabel.CENTER);
			setForeground(aTableHeader.getForeground());
			setBackground(aTableHeader.getBackground());
			setBorder(UIManager.getBorder("TableHeader.cellBorder"));
			fTableHeader = aTableHeader;
		}

		@Override
		public Component getTableCellRendererComponent(JTable aTable, Object aValue,
				boolean aIsSelected, boolean aHasFocus, int aRowIdx, int aColumnIdx) {
			setText((aValue == null) ? "" : aValue.toString());
			setFont(fTableHeader.getFont());
			return this;
		}

		private JTableHeader fTableHeader;
	}
}
